feat: add Bitwarden Password Manager provider#166
Conversation
Integrate Bitwarden Password Manager (bw CLI) as a secretspec provider. The provider supports vault-wide item access across all Bitwarden item types (Login, Card, Identity, SSH Key, Secure Note) with smart field extraction, URI-based configuration (bitwarden://[org@]collection?server=...&type=...&field=...), and session-based authentication via BW_SESSION. Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Upstream now ships a native BWS SDK provider (bws.rs), so the unified Password Manager + Secrets Manager provider must be reduced to Password Manager only. Remove the BitwardenService enum, BWS-specific config fields, execute_bws_command method, and bws:// scheme handling from register_provider! and TryFrom. Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Upstream's Provider trait has evolved from dynamic dispatch with (project, key, profile) signatures to a static-registration pattern with Address, ProviderUrl, and ProviderCredentials. Rewrite BitwardenConfig's TryFrom to accept &ProviderUrl, add convention_address/supported_coords/with_credentials/uri, and update get/set to accept Address<'_> parameters resolved via resolve_coords. Field resolution respects the native address coordinate first, then BITWARDEN_DEFAULT_FIELD, then config. Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Wire the bitwarden module into the provider registry with a bw feature flag (bw = ["dep:base64"], enabled by default). The base64 dependency encodes JSON for the bw CLI's item create/edit stdin path. Add four unit tests covering provider creation, collection, org-collection URI parsing, and bws:// scheme rejection. Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
The bitwarden.md page was drafted for a dual-service (bw + bws) provider. Strip empty code blocks, the Secrets Manager access token section, and a stray bullet left behind after BWS content removal. Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Add .pi/ to .gitignore for pi-agent task output. The collapsible_if lint in config::migrate_macos_config is a pre-existing upstream warning surfaced by clippy during this branch's development. Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Replace the manual setup requirements with an auto-provisioning setup_test_data() function that creates Bitwarden items if they don't exist, and registers an EXIT trap to clean them up unless --keep-test-data is passed. Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Replace invalid TOML keys ("Deploy SSH Key", etc.) with valid
bw_integration_test_* identifiers and ref = { item, field }
mapping, matching the new Provider API address model.
Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
Co-Authored-By: deepseek-v4-pro <noreply@deepseek.com>
The entry landed in the already-released 0.15.0 section during the rebase; the provider targets the next release.
The provider wraps the `bw` CLI, so the id now matches the tool it drives and the existing cargo feature, mirroring how the bws provider is named after the bws CLI. URIs change from bitwarden:// to bw://. The provider is unreleased, so there is no compatibility impact. Module, source file, and docs page follow the id (provider::bw, providers/bw.md). BITWARDEN_DEFAULT_TYPE/FIELD env vars keep their prefix to stay clear of the bw CLI's own BW_* namespace.
Add the Bitwarden Password Manager provider to every listing the adding-providers checklist names: sidebar and llms.txt description, concepts and reference tables (with security row), landing page metadata/hero/bento (provider count 14 -> 15), quick-start and README config-init examples, and the rustdoc provider summary. All entries carry the 0.16+ label; the provider page gains the version notice. The landing grid's existing plain 'Bitwarden' label becomes 'Bitwarden Secrets Manager' to disambiguate the two Bitwarden providers.
bw can prompt on stdin (e.g. for the master password when a session is missing or expired). stdin is null or closed on our invocations so a prompt could not hang us, but --nointeraction makes bw fail fast with its own clear error instead of a confusing EOF-related one, which matters in CI and other headless contexts.
Implements the FUTURE WORK plan from bitwarden_integration.sh: vaultwarden + caddy TLS proxy (the 2026+ bw CLI refuses plain http servers, so an internal self-signed cert is required), fixture account registered via the identity API (vaultwarden_bootstrap.py implements the client-side registration crypto bw doesn't expose), bw CLI isolated via BITWARDENCLI_APPDATA_DIR so the developer's real config is untouched. No repository secrets needed; works on fork PRs. Verified locally: full bitwarden_integration.sh suite 19/19 PASSED end-to-end from a cold start (containers, registration, login, suite, cleanup) on macOS + colima. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CDSY4yJXvof9Am8acxzHf
|
Vaultwarden testing done, as promised on #15: the full And since you asked about automating it: I've opened ashebanow#2 against your branch implementing the FUTURE WORK plan from the script header — disposable Vaultwarden + fixture account, no repository secrets, fork-PR-friendly. Two gotchas discovered on the way, handled in the harness:
|
| } | ||
|
|
||
| // Parse query parameters | ||
| for (key, value) in url.query_pairs() { |
There was a problem hiding this comment.
P1 — Enable query parsing in bw-only builds
When consumers build only the advertised feature with cargo check -p secretspec --no-default-features --features bw, this call fails because ProviderUrl::query_pairs is not compiled unless another provider feature or test is enabled. Please include bw in that method’s cfg gate so the feature compiles independently.
|
|
||
| // Configure server if specified | ||
| if let Some(server) = &self.config.server { | ||
| cmd.env("BW_SERVER", server); |
There was a problem hiding this comment.
P1 — Configure self-hosted servers through a supported bw mechanism
For bw://?server=..., setting BW_SERVER has no effect because the official bw CLI does not read that environment variable; BW_SERVER=https://example.invalid bw status still reports the configured default server. Consequently every advertised self-hosted operation targets the CLI’s existing server instead, so this needs a supported configuration mechanism.
| // Use Bitwarden's built-in search to find items matching the key | ||
| let mut list_args = vec!["list", "items", "--search", item_name]; | ||
|
|
||
| // Add organization filter if configured (from config or environment variable) |
There was a problem hiding this comment.
P1 — Apply the configured collection to vault searches
When bw://collection-id, bw://org@collection, or BITWARDEN_COLLECTION is used, this lookup applies only the organization filter and ignores the collection. If another collection contains the same item name, reads can return its value, and the equivalent unfiltered write search can update it. Please restrict searches with the configured collection ID.
| 0 => Some(BitwardenFieldType::Text), | ||
| 1 => Some(BitwardenFieldType::Hidden), | ||
| 2 => Some(BitwardenFieldType::Boolean), | ||
| _ => None, |
There was a problem hiding this comment.
P1 — Accept linked Bitwarden custom fields
Bitwarden defines custom-field type 3 for linked fields, but this parser rejects it. Since set_to_password_manager deserializes every item returned by bw list items, one linked field anywhere in the vault causes every write to abort with Unknown field type: 3; reads of an item containing one also fail.
| match field.to_lowercase().as_str() { | ||
| "username" => login_data["username"] = serde_json::Value::String(value.to_string()), | ||
| "totp" => login_data["totp"] = serde_json::Value::String(value.to_string()), | ||
| _ => login_data["password"] = serde_json::Value::String(value.to_string()), |
There was a problem hiding this comment.
P1 — Preserve explicitly named custom fields when creating items
When a new login is created with a documented custom field such as field=api_key, this fallback stores the value in login.password. A subsequent read using the same provider looks for the custom api_key field and returns nothing. Card and Identity creation use the same incorrect fallback pattern for unknown field names.
| .map(|s| s.to_string()) | ||
| .or_else(|| std::env::var("BITWARDEN_DEFAULT_FIELD").ok()) | ||
| .or_else(|| self.config.default_field.clone()) | ||
| .unwrap_or_else(|| "password".to_string()); |
There was a problem hiding this comment.
P1 — Choose type-specific defaults when updating existing items
For an existing Secure Note, Card, Identity, or SSH-key item without an explicit field, this always selects password. The updater therefore creates a custom password field while the corresponding getter defaults to notes/value, card number, email, or private key, so set succeeds but a following get returns the old value.
|
|
||
| // Look for existing field | ||
| for field_obj in fields.iter_mut() { | ||
| if field_obj["name"].as_str() == Some(field) { |
There was a problem hiding this comment.
P2 — Match custom-field names consistently when updating
Reads match custom-field names case-insensitively, but updates use case-sensitive equality here. With an existing field named API_KEY and a reference to api_key, reading works while setting adds a duplicate field; later reads encounter the original field first and continue returning the stale value.
|
|
||
| The `bw` provider integrates with Bitwarden Password Manager for secret management with vault-wide access to all item types. | ||
|
|
||
| :::note[Version compatibility] |
There was a problem hiding this comment.
P2 — Advertise the actual target release everywhere
The base already contains the released 0.16.0 section dated 2026-07-17, so this unreleased provider cannot be described as an upcoming 0.16 feature. Please update every new listing to the actual target release, including the homepage entries that currently omit a version entirely; otherwise users of 0.16 are told the unavailable provider is supported.
|
|
||
| ```toml | ||
| # secretspec.toml | ||
| [development] |
There was a problem hiding this comment.
P2 — Use valid profile configuration in the provider guide
These tables do not match the project configuration schema: profiles are under [profiles.<name>], and profile provider selection is configured through the profile’s defaults.providers alias list rather than a provider string. Users following this example will get an invalid or misinterpreted secretspec.toml instead of collection-specific profiles.
| ## [0.16.0] - 2026-07-17 | ||
|
|
||
| ### Added | ||
| >>>>>>> upstream/main |
There was a problem hiding this comment.
P2 — Remove the unresolved changelog merge marker
The literal >>>>>>> upstream/main is a merge artifact and will be published as part of the 0.16 changelog unless removed.
One test per finding (R1 linked-field type 3 poisons writes; R2 named custom field lost on create; R3 wrong default field on non-login update; R4 case-sensitive update vs case-insensitive read). Each reports REPRODUCED/FIXED and the set exits 0 only when all findings are fixed — opt-in via RUN_REGRESSIONS=1 in the harness until then. Verified against disposable Vaultwarden: currently 0 fixed / 4 reproduced, matching the review descriptions verbatim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CDSY4yJXvof9Am8acxzHf
|
Following up on the review: the four findings that are reproducible against a live vault are now regression tests in the Vaultwarden harness (added to ashebanow#2) — one test per finding, each reporting Current run against disposable Vaultwarden confirms every one of them verbatim: So @ashebanow has an executable checklist for those four ( |
|
I'm traveling this long weekend, not sure how much time I'll have. But b as
CK Tuesday at the latest.
…On Thu, Jul 23, 2026 at 9:45 AM bsorescu ***@***.***> wrote:
*bsorescu* left a comment (cachix/secretspec#166)
<#166 (comment)>
Following up on the review: the four findings that are reproducible
against a live vault are now regression tests in the Vaultwarden harness
(added to ashebanow#2 <ashebanow#2>) —
one test per finding, each reporting REPRODUCED/FIXED, exiting green only
when all are fixed.
Current run against disposable Vaultwarden confirms every one of them
verbatim:
── R1: linked custom field (type 3) poisons unrelated writes ──
REPRODUCED R1 — any write aborts while a linked field exists (Unknown field type: 3)
── R2: named custom field preserved when creating a new item ──
REPRODUCED R2 — value not readable via field=api_key after create (stored in login.password instead)
── R3: type-specific default on update (secure note) ──
REPRODUCED R3 — set wrote a password field; get still returns 'old-note-value'
── R4: case-insensitive field matching on update ──
REPRODUCED R4 — update added a duplicate field; get still returns 'old-value'
Findings fixed: 0 · reproduced: 4
So @ashebanow <https://github.com/ashebanow> has an executable checklist
for those four (RUN_REGRESSIONS=1 tests/vaultwarden_harness.sh) — they'll
flip to FIXED as the fixes land. The collection-filtering finding needs an
org + two collections in the fixture account; happy to extend the bootstrap
for that as a follow-up if useful. On the self-hosted config finding: the
only supported mechanism the CLI offers is bw config server (which
requires being logged out), so the provider likely needs to shell that out
— or document it as a user prerequisite, which is what Kamal's bitwarden
adapter effectively does.
—
Reply to this email directly, view it on GitHub
<#166?email_source=notifications&email_token=AAACX7Y6NB7OMN67M32A2J35GI6J5A5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKMBWGEYDCMRRHE2KM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5061012194>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAACX73WHV2OLXU7QP7CVW35GI6J5AVCNFSNUABGKJSXA33TNF2G64TZHMYTAMJTHE4DINJRGI5US43TOVSTWNBZGE2TKOBSG4YDBILWAI>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
|
@Tails I'm planning to do a 0.17 release on Monday if you'd like to get this in. No issue if we target 0.18. |
test: fully-automated integration run against disposable Vaultwarden
Adds a Bitwarden Password Manager provider (
bw://) backed by the officialbwCLI.This resumes the effort from #24 — the renewed discussion in #15 is what inspired picking this work back up. It starts from the Password Manager core of that PR and reworks it for today's codebase.
What's included
bw://provider shelling out to thebwCLI, with vault-wide access to all item types: logins, cards, identities, SSH keys, and secure notesbwmatches the CLI it drives and the cargo feature, mirroring howbws://is named after thebwsCLIbw://?type=login&field=username) andBITWARDEN_DEFAULT_TYPE/BITWARDEN_DEFAULT_FIELDenv vars (prefix kept clear of thebwCLI's ownBW_*namespace)ref = { item = "...", field = "..." }mapping insecretspec.toml, so secret keys stay valid identifiers while pointing at arbitrarily named vault itemsbw://org@collection) and self-hosted servers (?server=)secretspec setbwfailure modes: CLI not installed (with install instructions), not logged in, vault locked; secret values pass via stdin and the session via the environment, never argvbwcargo feature (default-enabled; adds only abase64dependency since the provider drives the CLI rather than linking an SDK)docs/providers/bw.mdwith 0.16 version notice) and all provider listings per the adding-providers checklist — sidebar, llms.txt description, concepts/reference tables, security table, landing page, quick-start, README — each labeled(0.16+)tests/bitwarden_integration.sh: end-to-end suite against a real vault covering all five item types,refmapping, env-var defaults, error paths, and set/update; auto-creates and cleans up its own test itemsbitwarden-cliadded to devenv for integration testingChanges relative to #24
bws://provider, so this PR is Password Manager onlyProviderUrl/AddressAPI and provider registrationrefmappingTesting
cargo test --allgreen locally (macOS)refmapping, env-var defaults, error handling, and set/update pathsNotes for reviewers
Refs #15 (inspiration for resuming), supersedes #24.
AI Note: this project's code and documentation were produced with the help of UI. The old pull request was developed with the help of Claude Code and Claude Opus/Sonnet, the additional work for this pull request was done using my custom pi environment and Deepseek v4 Pro/Flash.